package com.itheima.prize.msg;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.itheima.prize.commons.config.RedisKeys;
import com.itheima.prize.commons.db.entity.*;
import com.itheima.prize.commons.db.service.CardGameProductService;
import com.itheima.prize.commons.db.service.CardGameRulesService;
import com.itheima.prize.commons.db.service.CardGameService;
import com.itheima.prize.commons.db.service.GameLoadService;
import com.itheima.prize.commons.utils.RedisUtil;
import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.*;

/**
 * 活动信息预热，每隔1分钟执行一次
 * 查找未来1分钟内（含），要开始的活动
 */
@Component
public class GameTask {
    private final static Logger log = LoggerFactory.getLogger(GameTask.class);
    @Autowired
    private CardGameService gameService;
    @Autowired
    private CardGameProductService gameProductService;
    @Autowired
    private CardGameRulesService gameRulesService;
    @Autowired
    private GameLoadService gameLoadService;
    @Autowired
    private RedisUtil redisUtil;

    @Scheduled(cron = "0 * * * * ?")//这是一个注解（可以理解为一个标记），告诉程序这个方法需要定时执行//cron = "0 * * * * ?" 是定时规则，表示 "每分钟的第 0 秒执行一次"//简单说：就是这个方法会每分钟自动运行一次
    public void execute() {//这个方法就是定时执行时要做的具体工作
        System.out.printf("scheduled!" + new Date());//在控制台打印一句话//"scheduled!" 是固定文字，意思是 "定时任务执行了！"//new Date() 会获取当前的时间//合起来就是：每次任务执行时，在控制台显示 "定时任务执行了！" 加上当前时间
        //TODO：任务5.1-缓存预热-调度写入缓存

        // 获取当前时间的Calendar实例
        Calendar calendar = Calendar.getInstance();//Calendar 是一个处理日期时间的工具//获取一个当前系统时间的日历对象//拿到一个包含当前年、月、日、时、分、秒的 "电子日历"
        // 清除毫秒部分
        calendar.set(Calendar.MILLISECOND, 0);//这行是设置日历对象的毫秒数为 0//因为时间通常精确到毫秒（1 秒 = 1000 毫秒），这行代码的作用是去掉毫秒部分//比如原本时间是 10:05:30.123，处理后就变成 10:05:30.000
        // 获取不带毫秒的Date对象
        Date now = calendar.getTime();//把处理好的日历对象转换成一个Date类型的时间对象//这个now变量就表示 "去掉了毫秒部分的当前时间"//后续可以用这个now变量进行时间相关的操作
        //分布式锁，防止重复启动任务
        if (!redisUtil.setNx("game_task_" + now.getTime(), 1, 60L)) {//这是一个分布式锁的判断，防止多个服务器重复执行这个任务//尝试在 Redis 中创建一个键值对，键是 "game_task_" 加当前时间的数字形式//60L 表示这个键 60 秒后自动消失
            log.info("task started by another server!");//"这个任务已经被其他服务器执行了！"
            return;
        }
        //查询将来1分钟内要开始的活动
        QueryWrapper<CardGame> gameQueryWrapper = new QueryWrapper<>();
        //开始时间大于当前时间(活动还没有开始)
        gameQueryWrapper.gt("starttime", now);
        //小于等于（当前时间+1分钟）
        gameQueryWrapper.le("starttime", DateUtils.addMinutes(now, 1));
        List<CardGame> list = gameService.list(gameQueryWrapper);
        if (list.size() == 0) {
            //没有查到要开始的活动
            log.info("game list scan : size = 0");
            return;
        }
        log.info("game list scan : size = {}", list.size());
        //有相关活动数据，则将活动数据预热，进redis
        list.forEach(game -> {
            //活动开始时间
            long start = game.getStarttime().getTime();//获取这个活动的 "开始时间"，并转换成毫秒数（计算机容易处理的数字形式）
            //活动结束时间
            long end = game.getEndtime().getTime();
            //计算活动结束时间到现在还有多少秒，作为redis key过期时间
            long expire = (end - now.getTime()) / 1000;
//            long expire = -1; //永不过期
            //活动持续时间（ms）
            long duration = end - start;

            Map queryMap = new HashMap();//装查询条件的小盒子
            queryMap.put("gameid", game.getId());//往这个小盒子里放一个条件："gameid"（活动 ID）等于当前活动的 ID

            //活动基本信息
            game.setStatus(1);
            redisUtil.set(RedisKeys.INFO + game.getId(), game, -1);//这个key永不过期
            log.info("load game info:{},{},{},{}", game.getId(), game.getTitle(), game.getStarttime(), game.getEndtime());

            //活动奖品信息
            List<CardProductDto> products = gameLoadService.getByGameId(game.getId());
            Map<Integer, CardProduct> productMap = new HashMap<>(products.size());
            products.forEach(p -> productMap.put(p.getId(), p));
            log.info("load product type:{}", productMap.size());

            //奖品数量等配置信息
            List<CardGameProduct> gameProducts = gameProductService.listByMap(queryMap);//就是找到这个活动具体设置了哪些奖品，每种奖品有多少数量等配置
            log.info("load bind product:{}", gameProducts.size());

            //令牌桶
            List<Long> tokenList = new ArrayList();
            gameProducts.forEach(cgp -> {
                //生成amount个start到end之间的随机时间戳做令牌
                Integer amount = cgp.getAmount();
                if (amount == null) {
                    log.warn("奖品关联配置中数量为空，gameId={}, productId={}", cgp.getGameid(), cgp.getProductid());
                    amount = 0;
                }
                for (int i = 0; i < amount; i++) {
                    long rnd = start + new Random().nextInt((int) duration);
                    //为什么乘1000，再额外加一个随机数呢？ - 防止时间段奖品多时重复
                    //记得取令牌判断时间时，除以1000，还原真正的时间戳
                    long token = rnd * 1000 + new Random().nextInt(999);
                    //将令牌放入令牌桶
                    tokenList.add(token);
                    //以令牌做key，对应的商品为value，创建redis缓存
                    log.info("token -> game : {} -> {}", token / 1000, productMap.get(cgp.getProductid()).getName());
                    //token到实际奖品之间建立映射关系
                    redisUtil.set(RedisKeys.TOKEN + game.getId() + "_" + token, productMap.get(cgp.getProductid()), expire);
                }
            });
            //排序后放入redis队列
            Collections.sort(tokenList);
            log.info("load tokens:{}", tokenList);

            //从右侧压入队列，从左到右，也就是小的先从右边进去，时间戳逐个增大
            if (!tokenList.isEmpty()) {
                redisUtil.rightPushAll(RedisKeys.TOKENS + game.getId(), tokenList);
            }
            redisUtil.expire(RedisKeys.TOKENS + game.getId(), expire);//令牌过期时间

            //奖品策略配置信息
            List<CardGameRules> rules = gameRulesService.listByMap(queryMap);//查询当前活动的规则配置（通过之前的活动 ID 条件）//这些规则包括不同用户等级的中奖次数限制、参与次数限制等
            //遍历策略，存入redis hset
            rules.forEach(r -> {//对对对，这里是活动的规则
                redisUtil.hset(RedisKeys.MAXGOAL + game.getId(), r.getUserlevel() + "", r.getGoalTimes());//最大中奖次数
                redisUtil.hset(RedisKeys.MAXENTER + game.getId(), r.getUserlevel() + "", r.getEnterTimes());//最大参与次数
                redisUtil.hset(RedisKeys.RANDOMRATE + game.getId(), r.getUserlevel() + "", r.getRandomRate());//随机概率
                log.info("load rules:level={},enter={},goal={},rate={}",
                        r.getUserlevel(), r.getEnterTimes(), r.getGoalTimes(), r.getRandomRate());
            });
            redisUtil.expire(RedisKeys.MAXGOAL + game.getId(), expire);
            redisUtil.expire(RedisKeys.MAXENTER + game.getId(), expire);
            redisUtil.expire(RedisKeys.RANDOMRATE + game.getId(), expire);


            //活动状态变更为已预热，禁止管理后台再随便变动
            game.setStatus(1);
            gameService.updateById(game);

        });
    }
}
